"""Sends a message in morse to a ledstrip.

This file can be used as a python3 script, or as a module which then provides
the MorseSender class.  Edit __init__() for it to use the boards' IP-address.

MorseSender sends a morse encoded message to a ledstrip. The ledstrip must be
attached to the D6-pin of an ESP8266 board. The ESP must be running the
StandardFirmataWiFi.ino sketch. If you want to upload that sketch using the
Arduino IDE, follow instructions first to make the board known to the IDE:
https://github.com/esp8266/Arduino#installing-with-boards-manager

TODO:
- Make this class listen for commandline input with instructions to turn off,
  send a new message, change brightness, speed, etc.
"""

import time
import sys
import socket
from utils import prnt, msg2morse, new_brightness, mylog
from config import Config

# These constants are copied from FirmataConstants.h (Arduino Firmata lib):
SET_PIN_MODE = 0xF4
PIN_MODE_PWM = 0x03
ANALOG_MESSAGE = PWM_MESSAGE = 0xE0
DIGITAL_MESSAGE = 0x90
REPORT_DIGITAL = 0xD0


class MorseSender():
    """MorseSender sends a morse encoded message to a ledstrip."""

    def __init__(self, address, port, secs_dit):
        """Initialization of the MorseSender"""

        mylog.info('Initializing MorseSender.')

        self._address = address
        self._port = port
        self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self._sock.connect((self._address, self._port))

        command = bytes([DIGITAL_MESSAGE, 4, 0])    # Turn on pin 2 in order to
        self._sock.sendall(command)                 # turn off bright blue led.

        self._secs_dit = secs_dit
        self._secs_dah = 3 * self._secs_dit
        self._secs_letter_sep = 3 * self._secs_dit
        self._secs_word_sep = 7 * self._secs_dit

        self._ledstrip = 12             # the GPIO-number of D6 is 12 (D3 is 0)
        self._set_mode_pwm(self._ledstrip)
        self._pwm_write(self._ledstrip, 0)

    def dit(self, brightness):
        """Sends a 'dit' (short pulse)."""

        prnt('.')
        self._pwm_write(self._ledstrip, brightness)
        time.sleep(self._secs_dit)

    def dah(self, brightness):
        """Sends a 'dah' (long pulse)."""

        prnt('-')
        self._pwm_write(self._ledstrip, brightness)
        time.sleep(self._secs_dah)

    def sep_ditdahs(self):
        """Sends a short pause between 'dits' and 'dahs'."""

        self._pwm_write(self._ledstrip, 0)
        time.sleep(self._secs_dit)

    def sep_letters(self):
        """Sends a medium length pause between letters."""

        prnt(' ')
        self._pwm_write(self._ledstrip, 0)
        time.sleep(self._secs_letter_sep)

    def sep_words(self):
        """Sends a long pause between words."""

        prnt('       ')
        self._pwm_write(self._ledstrip, 0)
        time.sleep(self._secs_word_sep)

    def inter_message(self, brightness, secs):
        """Sends a continuous glow."""

        prnt('\n')
        mylog.info(f'Pausing for {secs} secs with brightness {brightness}.')
        self._pwm_write(self._ledstrip, brightness)
        # progress indicator:
        for _ in range(72):
            prnt('z')
            time.sleep(secs / 72)
        prnt('\n')

    def sendmsg(self, brightness, msg=''):
        """Encodes message into morse and sends it to the board/ledstrip using
        the given brightness.
        """

        mylog.info(f'Sending message {msg} with brightness {brightness}.')
        prev_ch = ''
        for ch in msg2morse(msg):
            if ch == '.':
                if prev_ch in ('.', '-'):
                    self.sep_ditdahs()
                self.dit(brightness)
            elif ch == '-':
                if prev_ch in ('.', '-'):
                    self.sep_ditdahs()
                self.dah(brightness)
            elif ch == ' ':
                self.sep_letters()
            elif ch == 'S':
                self.sep_words()
            else:
                pass
            prev_ch = ch
        prnt('\n')

    def _set_mode_pwm(self, pin_number):
        pin_mode = PIN_MODE_PWM
        command = bytes([SET_PIN_MODE, pin_number, pin_mode])
        self._sock.sendall(command)

    def _pwm_write(self, pin, value):
        command = bytes([PWM_MESSAGE + pin, value & 0x7F, (value >> 7) & 0x7F])
        self._sock.sendall(command)

    def shutdown(self, led_off=True):
        """Turns off the ledstrip and shuts down the board."""

        if led_off:
            self._pwm_write(self._ledstrip, 0)
            time.sleep(0.5)                     # circuit or socket needs time
            mylog.info('ledstrip turned off')
        for port in range(3):
            command = bytes([REPORT_DIGITAL + port, 0x00])
            self._sock.sendall(command)
        self._sock.shutdown(socket.SHUT_RDWR)
        self._sock.close()
        mylog.info('board shut down')


def main():                                                 # pragma: no cover
    try:
        ms = MorseSender(Config.ADDRESS, Config.PORT, Config.DIT)
        mylog.info(f'Created sender with DIT={Config.DIT}.')
        while True:
            ms.sendmsg(new_brightness(), Config.THE_MESSAGE)
            ms.sep_words()
            ms.inter_message(new_brightness(), Config.PAUSE)
    except KeyboardInterrupt:
        mylog.info('keyboard interrupt, shutting down')
        ms.shutdown()
        return 0
    except Exception as e:
        mylog.error(f'Unexpected exception: {type(e).__name__}: {e}')
        return 1


if __name__ == '__main__':                                  # pragma: no cover
    sys.exit(main())
